package com.danicsoft.daide.token;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.io.StringReader;
import java.util.LinkedList;
import java.util.List;

import com.danicsoft.daide.binary.ByteMessageHandler;
import com.danicsoft.daide.binary.Connection;
import com.danicsoft.daide.binary.DisconnectedException;
import com.danicsoft.daide.historian.TokenHistorian;
import com.danicsoft.daide.token.TokenMap.Commands;
import com.danicsoft.daide.token.TokenMap.Miscellaneous;
import com.danicsoft.daide.token.TokenMap.Parameters;

/**
 * This class is responsible for parsing tokens.
 */
public class TokenParser implements ByteMessageHandler {

	/**
	 * The map we will use for parsing tokens.
	 */
	private TokenMap myMap;

	/**
	 * A list of all the listeners we currently have.
	 */
	private List<DiplomaticMessageListener> listeners = new LinkedList<DiplomaticMessageListener>();

	/**
	 * The connection we are parsing from
	 */
	private Connection myConnection;

	/**
	 * The historian we use for parsing messages.
	 */
	private TokenHistorian historian;

	/**
	 * Adds a listener for diplomatic messages.
	 * 
	 * @param listener
	 *        The listener we are adding.
	 */
	public synchronized void addMessageListener(DiplomaticMessageListener listener) {

		listeners.add(listener);

	}

	/**
	 * Receive a diplomatic message. The message will be passed on to all
	 * <code>MessageListener</code>s that are registered.
	 * 
	 * @param tokenGroup
	 *        The <code>TokenGroup</code> that makes up the diplomatic message
	 */
	public synchronized void diplomaticMessageReceived(TokenGroup tokenGroup) {

		for (DiplomaticMessageListener ml : listeners)
			ml.diplomaticMessageReceived(tokenGroup);

	}

	/**
	 * Reads a <code>TokenGroup</code> from a string representation.
	 * 
	 * @param tokenString
	 *        The text representation of the <code>TokenGroup</code> to read.
	 * @return A <code>TokenGroup</code> representing the tokens in the text.
	 * @throws TextException
	 *         If an unhandled character appears in the text, or the tokens
	 *         specified are not three letters.
	 */
	public TokenGroup readFromText(String tokenString) throws TextException {
		StringReader stringReader = new StringReader(tokenString);
		try {
			return readFromText(stringReader);
		}
		catch (IOException e) {
			return new TokenGroup();
		}

	}

	/**
	 * Construct a new TokenParser that uses the specified mapping.
	 * 
	 * @param map
	 *        The mapping to use.
	 * @param conn
	 *        The connection we will use to send error messages on.
	 * @param historian
	 *        The historian we are going to use to log parsing problems.
	 */
	public TokenParser(TokenMap map, Connection conn, TokenHistorian historian) {
		myMap = map;
		myConnection = conn;
		this.historian = historian;
	}

	/**
	 * Construct a new TokenParser that uses the specified mapping.
	 * 
	 * @param map
	 *        The mapping to use.
	 * @param conn
	 *        The connection we will use to send error messages on.
	 */
	public TokenParser(TokenMap map, Connection conn) {
		this(map, conn, new TokenHistorian());
	}

	/**
	 * Construct a new TokenParser that uses the default mapping.
	 * 
	 * @param conn
	 *        The connection we will use to send error messages on.
	 */
	public TokenParser(Connection conn) {
		this(new TokenMap(), conn, new TokenHistorian());
	}

	/**
	 * Construct a new TokenParser that uses the default mapping, and cannot
	 * send or recieve messages.
	 */
	public TokenParser() {
		this(null);
	}

	/**
	 * An enumeration of the different types a token can have.
	 * 
	 * @author Daniel Yule (daniel.yule@gmail.com)
	 * @version 1.0
	 */
	private enum tokenTypes {
		/**
		 * This is an integer token
		 */
		INTEGER, /**
					 * This is a standard token
					 */
		STANDARD,
		/**
		 * This is a text token.
		 */
		TEXT,
		/**
		 * There is no current type of token.
		 */
		NONE
	}

	/**
	 * Reads a <code>TokenGroup</code> from a string representation.
	 * 
	 * @param stringReader
	 *        The text representation of the <code>TokenGroup</code> to read,
	 *        read from via the reader.
	 * @return A <code>TokenGroup</code> representing the tokens in the text.
	 * @throws IOException
	 *         If the reader is unable to read from its input source.
	 * @throws TextException
	 *         If an unhandled character appears in the text, or the tokens
	 *         specified are not three letters.
	 */
	private TokenGroup readFromText(Reader stringReader) throws IOException, TextException {
		TokenGroup tokens = new TokenGroup();
		char thisChar = (char) stringReader.read();
		thisChar = Character.toUpperCase(thisChar);
		tokenTypes thisType = tokenTypes.NONE;
		StringBuilder thisToken = new StringBuilder();
		try {
			while (thisChar != ')' && thisChar != 0xFFFF) {
				switch (thisType) {
					case NONE:
						if (thisChar == '(') {
							tokens.add(readFromText(stringReader));
						}
						else if (thisChar == '\'' || thisChar == '"' || thisChar == '`') {
							thisToken = new StringBuilder();
							thisType = tokenTypes.TEXT;
						}
						else if (Character.isLetter(thisChar)) {
							thisToken = new StringBuilder();
							thisToken.append(thisChar);
							thisType = tokenTypes.STANDARD;
						}
						else if (Character.isDigit(thisChar) || thisChar == '-') {
							thisToken = new StringBuilder();
							thisToken.append(thisChar);
							thisType = tokenTypes.INTEGER;

						}
						else if (Character.isWhitespace(thisChar)) {

						}
						else {
							throw new TextException("Unknown character encountered");
						}
						thisChar = (char) stringReader.read();
						break;
					case TEXT:
						if (thisChar == '\'' || thisChar == '"' || thisChar == '`') {
							tokens.add(new TextToken(thisToken.toString()));
							thisType = tokenTypes.NONE;
						}
						else {
							thisToken.append(thisChar);
						}
						thisChar = (char) stringReader.read();
						break;
					case INTEGER:
						if (Character.isDigit(thisChar)) {
							thisToken.append(thisChar);
							thisChar = (char) stringReader.read();
						}
						else {
							tokens.add(new IntegerToken(Integer.parseInt(thisToken.toString())));
							thisType = tokenTypes.NONE;
						}
						break;
					case STANDARD:
						if (Character.isDigit(thisChar) || Character.isLetter(thisChar)) {
							if (thisToken.length() == 3) {
								throw new TextException("A token was specified with more than three letters.");
							}
							thisToken.append(thisChar);
							thisChar = (char) stringReader.read();
						}
						else {
							tokens.add(myMap.convert(thisToken.toString()));

							thisType = tokenTypes.NONE;
						}
						break;

				}
				thisChar = Character.toUpperCase(thisChar);
			}

			switch (thisType) {
				case TEXT:
					tokens.add(new TextToken(thisToken.toString()));
					break;
				case INTEGER:
					tokens.add(new IntegerToken(Integer.parseInt(thisToken.toString())));
					break;
				case STANDARD:
					tokens.add(myMap.convert(thisToken.toString()));

					break;
				case NONE:

			}
		}
		catch (InvalidTokenException e) {
			throw new TextException("Encountered an unknown token");
		}

		return tokens;
	}

	/**
	 * Reads a <code>TokenGroup</code> from the <code>InputStream</code>
	 * given.
	 * 
	 * @param iStream
	 *        The input stream to read from.
	 * @param topLevel
	 *        True if this is the top level (important for parenthesis checking)
	 * @return A <code>TokenGroup</code> consisting of the tokens read out of
	 *         this stream. If this stream of tokens contained parenthesis then
	 *         some of the tokens in this token group may themselves be token
	 *         groups.
	 * @throws IOException
	 *         If there was a problem reading from the input stream.
	 * @throws ParenthesisException
	 *         If the parenthesis were not correctly nested.
	 * @throws InvalidTokenException
	 */
	public TokenGroup readTokenGroup(InputStream iStream, boolean topLevel) throws IOException, ParenthesisException, InvalidTokenException {
		TokenGroup tokens = new TokenGroup();
		if (iStream.available() > 0) {
			Token curToken = Token.readToken(iStream, myMap);
			if (curToken instanceof StandardToken && !myMap.isValidToken((StandardToken) curToken)) {
				throw new InvalidTokenException(curToken);
			}
			while (curToken.getType() != TokenMap.MISC || (Byte) curToken.getData() != TokenMap.Miscellaneous.KET) {
				if (curToken.getType() == TokenMap.MISC && (Byte) curToken.getData() == TokenMap.Miscellaneous.BRA) {
					curToken = readTokenGroup(iStream, false);
				}
				tokens.add(curToken);
				if (!(iStream.available() > 0)) {
					if (!topLevel) {
						throw new ParenthesisException();
					}
					else {
						break;
					}
				}
				else {
					curToken = Token.readToken(iStream, myMap);
				}
			}
		}
		if (iStream.available() > 0 && topLevel) {
			throw new ParenthesisException();
		}

		return tokens;
	}

	/**
	 * Sends a message to the AI server. The array of tokens supplied will be
	 * sent as a <code>Diplomatic Message</code> to the server.
	 * 
	 * @param message
	 *        The tokens to send to the server.
	 * @throws DisconnectedException
	 *         if we're already disconnected from the server when attempting to
	 *         send this message.
	 */
	public void send(TokenGroup message) throws DisconnectedException {
		byte[] dm = createDiplomaticMessage(message);
		myConnection.send(dm);
		historian.logMessageSent(this, message);
	}

	/**
	 * Converts the bytes given to a flat list of tokens and inserts it into the
	 * <code>TokenGroup</code>
	 * 
	 * @param message
	 *        the bytes to parse
	 * @return A <code>TokenGroup</code> containing a flat list of tokens.
	 */
	public TokenGroup toTokens(byte[] message) {
		TokenGroup rGroup = new TokenGroup();
		ByteArrayInputStream iStream = new ByteArrayInputStream(message);

		try {
			while (iStream.available() > 0) {
				Token addToken = Token.readToken(iStream, myMap);
				if (addToken instanceof StandardToken && !myMap.isValidToken((StandardToken) addToken)) {
					rGroup.add(TokenMap.getParameterToken(Parameters.ERR));
				}
				rGroup.add(addToken);
			}
		}
		catch (IOException e) {

		}
		return rGroup;
	}

	public byte[] getRM() {
		return myMap.getRM();
	}

	/**
	 * This method handles byte messages recieved from a Connection. If there
	 * are mis-matched parenthesis or if there is an invalid token, it sends a
	 * message on the connection to inform the other end of the problem.
	 * 
	 * @param message
	 *        The message we hae recieved as a series of tokens.
	 * @see com.danicsoft.daide.binary.ByteMessageHandler#handleMessage(byte[])
	 */
	public void handleMessage(byte[] message) {
		ByteArrayInputStream iStream = new ByteArrayInputStream(message);
		try {

			TokenGroup tGroup = readTokenGroup(iStream, true);
			historian.logMessageParsed(this, tGroup);
			diplomaticMessageReceived(tGroup);

		}
		catch (IOException e) {

		}
		catch (ParenthesisException e) {

			TokenGroup tList = toTokens(message);

			if (message.length > 2) {
				if (message[0] == TokenMap.COMMANDS && message[1] == Commands.PRN) {
					diplomaticMessageReceived(tList);
					return;
				}
			}
			try {
				historian.logParenthesisProblem(this, tList);
				TokenGroup prnGroup = new TokenGroup();
				prnGroup.add(TokenMap.getCommandToken(Commands.PRN));
				prnGroup.add(tList);
				send(prnGroup);
			}
			catch (DisconnectedException e1) {

			}
		}
		catch (InvalidTokenException e) {

			int used = message.length - iStream.available() - 2;
			historian.logParsingProblem(this, "Encountered an unknown token.\nType: " + message[used] + " Subtype: " + message[used + 1]);
			ByteArrayOutputStream oStream = new ByteArrayOutputStream(message.length + 8);
			try {
				// Create a huh message to send.

				TokenMap.getCommandToken(Commands.HUH).output(oStream);
				TokenMap.getParenthesisToken(true).output(oStream);
				oStream.write(message, 0, used);
				TokenMap.getParameterToken(Parameters.ERR).output(oStream);
				oStream.write(message, used, iStream.available() + 2);
				TokenMap.getParenthesisToken(false).output(oStream);
				myConnection.send(oStream.toByteArray());
			}
			catch (IOException e1) {

			}
			catch (DisconnectedException e1) {

			}

		}

	}

	public void setConnection(Connection conn) {
		myConnection = conn;

	}

	public void setRM(byte[] RM) {
		myMap = new TokenMap(RM);

	}

	/**
	 * Creates a diplomatic message.
	 * 
	 * @param message
	 *        The body of the message
	 * @return A well-formatted <code>Diplomatic Message</code>
	 */
	protected byte[] createDiplomaticMessage(TokenGroup message) {
		ByteArrayOutputStream bOut = new ByteArrayOutputStream();
		bOut.write(0x02); // Msg type
		bOut.write(0x00); // pad
		bOut.write(0); // Length placeholder
		bOut.write(0); // Length placeholder
		try {
			message.output(bOut);
		}
		catch (IOException e) {

		}
		byte[] bytes = bOut.toByteArray();
		bytes[2] = (byte) ((bytes.length - 4) >> 8); // Remaining length
		bytes[3] = (byte) ((bytes.length - 4) & 0xff); // Remaining length
		return bytes;
	}

	/**
	 * Generates an array of Strings into a properly nested TokenGroup.
	 * 
	 * @param message
	 *        The list of strings to tokenize.
	 * @return A TokenGroup containing the message as tokens.
	 * @throws InvalidTokenException
	 */
	public TokenGroup tokenizeStrings(String[] message) throws InvalidTokenException {
		TokenGroup mGroup = new TokenGroup();
		tokenizeStrings(message, 0, mGroup);
		return mGroup;
	}

	/**
	 * Converts part of an array of strings to tokens. It continues running
	 * until it hits one more ending bracket than starting bracket.
	 * 
	 * @param message
	 *        An array of String containing a message.
	 * @param index
	 *        The current index within the array we are to parse.
	 * @param group
	 *        The group of tokens we are adding to.
	 * @return The index where we stopped tokenizing.
	 * @throws InvalidTokenException
	 *         If a String that does not match a valid token is found.
	 */
	private int tokenizeStrings(String[] message, int index, TokenGroup group) throws InvalidTokenException {
		int i;
		for (i = index; i < message.length; i++) {
			Token thisToken;
			if(message[i].charAt(0) == '\''){
				thisToken = new TextToken(message[i].substring(1, message[i].length() - 1));
			}
			else if(Character.isDigit(message[i].charAt(0))) {
				thisToken = new IntegerToken(Integer.parseInt(message[i]));
			}
			else {
				thisToken = myMap.convert(message[i]);
			}

			if (thisToken.getType() == TokenMap.MISC) {
				if (((StandardToken) thisToken).getData() == Miscellaneous.BRA) {
					TokenGroup subGroup = new TokenGroup();
					i = tokenizeStrings(message, i + 1, subGroup);
					group.add(subGroup);
				}
				else {
					break;
				}
			}
			else {
				group.add(thisToken);
			}

		}
		return i;
	}

}
